﻿namespace Groundwork.Extensions
{
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Linq;

    /// <summary>
    /// Collection Extension Methods. These provide a way of operating on lists, collections and arrays of data.
    /// </summary>
    public static class Collections
    {
        /// <summary>
        /// Adds a value to a dictionary if it does not already exist in the Dictionary
        /// </summary>
        /// <typeparam name="K">key generic type</typeparam>
        /// <typeparam name="V">value generic type</typeparam>
        /// <param name="dict">source dictionary</param>
        /// <param name="key">key</param>
        /// <param name="value">value to add</param>
        public static void AddIfNotExists<K, V>(this IDictionary<K,V> dict, K key, V value)
        {
            if (!dict.ContainsKey(key))
            {
                dict.Add(key, value);
            }
        }

        /// <summary>
        /// Adds a value to a list if it does not already exist in the Dictionary
        /// </summary>
        /// <typeparam name="T">type</typeparam>
        /// <param name="list">source list</param>
        /// <param name="key">key</param>
        /// <param name="T">value to add</param>
        public static void AddIfNotExists<T>(this IList<T> list, T value)
        {
            if (!list.Contains(value))
            {
                list.Add(value);
            }
        }

        /// <summary>
        /// Adds a value to a collection if it does not already exist in the collection
        /// </summary>
        /// <typeparam name="T">type</typeparam>
        /// <param name="list">source collection</param>
        /// <param name="value">value to add</param>
        public static void AddIfNotExists<T>(this ICollection<T> list, T value)
        {
            if (!list.Contains(value))
            {
                list.Add(value);
            }
        }


        /// <summary>
        /// Adds a value to an object array if it does not exist in the object array
        /// </summary>
        /// <param name="list">source array</param>
        /// <param name="value">value to add</param>
        public static void AddIfNotExists(this object[] list, object value)
        {
            if (!list.Contains(value))
            {
                list.SetValue(value, list.Length + 1);
            }
        }

        /// <summary>
        /// Splits the specified list into a number of smaller lists. The size of the lists is controlled by the chunking parameter
        /// </summary>
        /// <typeparam name="T">Generic type</typeparam>
        /// <param name="list">The current list</param>
        /// <param name="chunking">The size of the smaller sub lists that are to be created.</param>
        /// <returns>A list of lists split to the chunking size</returns>
        /// <exception cref="System.ArgumentException">chunkSize must be greater than 0.</exception>
        /// <exception cref="System.ArgumentException">chunkSize must be greater than 0.</exception>
        /// <example>
        ///   <code>
        ///   <![CDATA[
        /// List<string> myList = new List<string>(){ "A", "B", "C", "D", "E", "F", "G" };
        /// List<List<string>> mySplitLists = myList.Split(2);
        /// results in:
        /// List 1 = A, B
        /// List 2 = C, D
        /// List 3 = E, F
        /// List 4 = G
        /// ]]>
        ///   </code>
        ///   </example>
        /// <remarks>If the split is uneven the lists will not be filled will tailing nulls so that the list meets the chunking size.</remarks>
        public static List<List<T>> Split<T>(this List<T> list, int chunking)
        {
            if (chunking <= 0)
            {
                throw new ArgumentException("chunkSize must be greater than 0.");
            }

            List<List<T>> retVal = new List<List<T>>();
            int index = 0;
            while (index < list.Count)
            {
                int count = list.Count - index > chunking ? chunking : list.Count - index;
                retVal.Add(list.GetRange(index, count));

                index += chunking;
            }

            return retVal;
        }

        /// <summary>
        /// Determines whether the specified List contains any of the values in the toFind list
        /// </summary>
        /// <typeparam name="T">Generic Type</typeparam>
        /// <param name="values">Current List</param>
        /// <param name="toFind">List of values to match</param>
        /// <returns><c>true</c> if the specified values contains any; otherwise, <c>false</c>.</returns>
        /// <example>
        ///   <code>
        ///   <![CDATA[
        /// List<string> myList = new List<string>(){ "A", "B", "C" };
        /// List<string> myMatch = new List<string>(){ "X" , "B", "G" };
        /// bool hasFoundMatch = myList.ContainsAny(myMatch);
        /// results in:
        /// true
        /// List<string> myList = new List<string>(){ "A", "B", "C" };
        /// List<string> myMatch = new List<string>(){ "X" , "M", "G" };
        /// bool hasFoundMatch = myList.ContainsAny(myMatch);
        /// results in:
        /// false
        /// ]]>
        ///   </code>
        ///   </example>
        public static bool ContainsAny<T>(this List<T> values, List<T> toFind)
        {
            IEnumerable<T> result = values.Intersect(toFind);
            if (result.Count() > 0)
            {
                return true;
            }

            return false;
        }

        /// <summary>
        /// Determines whether the specified Collection contains any of the values in the toFind list
        /// </summary>
        /// <typeparam name="T">Generic Type</typeparam>
        /// <param name="values">Current Collection</param>
        /// <param name="toFind">Collection of values to match</param>
        /// <returns><c>true</c> if the specified values contains any; otherwise, <c>false</c>.</returns>
        /// <example>
        ///   <code>
        ///   <![CDATA[
        /// ObservableCollection<string> myList = new ObservableCollection<string>(){ "A", "B", "C" };
        /// ObservableCollection<string> myMatch = new ObservableCollection<string>(){ "X" , "B", "G" };
        /// bool hasFoundMatch = myList.ContainsAny(myMatch);
        /// results in:
        /// true
        /// ObservableCollection<string> myList = new ObservableCollection<string>(){ "A", "B", "C" };
        /// ObservableCollection<string> myMatch = new ObservableCollection<string>(){ "X" , "M", "G" };
        /// bool hasFoundMatch = myList.ContainsAny(myMatch);
        /// results in:
        /// false
        /// ]]>
        ///   </code>
        ///   </example>
        public static bool ContainsAny<T>(this ICollection<T> values, ICollection<T> toFind)
        {
            IEnumerable<T> result = values.Intersect(toFind);
            if (result.Count() > 0)
            {
                return true;
            }

            return false;
        }

        /// <summary>
        /// Determines whether the specified string array contains any of the values in the toFind string array
        /// </summary>
        /// <typeparam name="T">Generic Type</typeparam>
        /// <param name="values">Current string array</param>
        /// <param name="toFind">List of values to match</param>
        /// <returns><c>true</c> if the specified values contains any; otherwise, <c>false</c>.</returns>
        /// <example>
        ///   <code>
        ///   <![CDATA[
        /// string[] myList = new string[] { "A", "B", "C" };
        /// string[] myMatch = new string[] { "X" , "B", "G" };
        /// bool hasFoundMatch = myList.ContainsAny(myMatch);
        /// results in:
        /// true
        /// string[] myList = new string[] { "A", "B", "C" };
        /// string[] myList = new string[] { "X" , "M", "G" };
        /// bool hasFoundMatch = myList.ContainsAny(myMatch);
        /// results in:
        /// false
        /// ]]>
        ///   </code>
        ///   </example>
        public static bool ContainsAny<T>(this string[] values, string[] toFind)
        {
            IEnumerable<string> result = values.Intersect(toFind);
            if (result.Count() > 0)
            {
                return true;
            }

            return false;
        }

        /// <summary>
        /// Converts a List to an ObservableCollection
        /// </summary>
        /// <typeparam name="T">Typed Collection</typeparam>
        /// <param name="value">List to convert</param>
        /// <returns>observable collection</returns>
        public static ObservableCollection<T> ToObservable<T>(this List<T> value)
        {
            ObservableCollection<T> newCollection = new ObservableCollection<T>();
            value.ForEach(i => newCollection.Add(i));
            return newCollection;
        }

        /// <summary>
        /// Converts a List to an ObservableCollection
        /// </summary>
        /// <typeparam name="TSource">Source Type</typeparam>
        /// <typeparam name="TConvert">Type to convert to</typeparam>
        /// <param name="value">List to convert</param>
        /// <returns>observable collection</returns>
        /// <remarks>Only types that implement IConvertable can be converted</remarks>
        public static ObservableCollection<TConvert> ToObservable<TSource, TConvert>(this List<TSource> value)
            where TSource : IConvertible
            where TConvert : IConvertible
        {
            ObservableCollection<TConvert> newCollection = new ObservableCollection<TConvert>();
            value.ForEach(i => newCollection.Add((TConvert)Convert.ChangeType(i, typeof(TConvert))));
            return newCollection;
        }

        /// <summary>
        /// Performs the specified action on each character of the string.
        /// </summary>
        /// <param name="value">string to perform the action on</param>
        /// <param name="operation">The Action delegate to perform on each element of the string</param>
        /// <param name="onError">(Optional) The Action delegate to perform if an error is encountered</param>
        /// <exception cref="System.ArgumentNullException">chunking is less than or equal to zero</exception>
        /// <example>
        ///   <code>
        ///   <![CDATA[
        /// string targetString = "Hello World";
        /// Action<Exception> onError = (ex)=>
        /// {
        /// Handle the exception
        /// };
        /// Action<char> process = (arg) =>
        /// {
        /// if (arg == '3')
        /// {
        /// throw new NullReferenceException();
        /// }
        /// };
        /// targetChar.ForEach(process, onError);
        /// ]]>
        ///   </code>
        ///   </example>
        /// <remarks>Performs a ForEach operation on each element of the string. Each character is passed to the processing Action delegate.
        /// If an exception is raised during the execution and a Action delegate is provided then the exception is passed to it so that it can
        /// be handled. If no exception delegate is passed then the exception is thrown and will have to be handled. via your own exception handler.</remarks>
        public static void ForEach(this string value, Action<char> operation, Action<Exception> onError = null)
        {
            value.ThrowIfNullOrEmpty();
            for (int i = 0; i < value.Length; i++)
            {
                try
                {
                    operation(value[i]);
                }
                catch (Exception exception)
                {
                    if (onError != null)
                    {
                        onError(exception);
                    }
                    else
                    {
                        throw;
                    }
                }
            }
        }

        /// <summary>
        /// Performs the specified action on each element of the List.
        /// </summary>
        /// <typeparam name="T">Generic Type argument</typeparam>
        /// <param name="list">string to perform the action on</param>
        /// <param name="operation">The operation.</param>
        /// <param name="onError">The Action delegate to perform if an error is encountered</param>
        /// <exception cref="System.Exception">Generic exception; Catches any exception occurring in the Action Delegate</exception>
        /// <example>
        ///   <code>
        ///   <![CDATA[
        /// List<string> targetString = new List<string>() { "a", "b", "c", "d", "e", "f", "g" };
        /// Action<Exception> onError = (ex)=>
        /// {
        /// Handle the exception
        /// };
        /// Action<char> process = (arg) =>
        /// {
        /// };
        /// targetChar.ForEach(process, onError);
        /// ]]>
        ///   </code>
        ///   </example>
        /// <remarks>Performs a ForEach operation on each element of the list. Each item is passed to the processing Action delegate.
        /// If an exception is raised during the execution and a Action delegate is provided then the exception is passed to it so that it can
        /// be handled. If no exception delegate is passed then the exception is thrown and will have to be handled. via your own exception handler.</remarks>
        public static void ForEach<T>(this IList<T> list, Action<T> operation, Action<Exception> onError = null)
        {
            foreach (var item in list)
            {
                try
                {
                    operation(item);
                }
                catch (Exception exception)
                {
                    if (onError != null)
                    {
                        onError(exception);
                    }
                    else
                    {
                        throw;
                    }
                }
            }
        }

        /// <summary>
        /// Performs the specified action on each element of the ICollection.
        /// </summary>
        /// <typeparam name="T">Generic Type argument</typeparam>
        /// <param name="collection">string to perform the action on</param>
        /// <param name="operation">The Action delegate to perform on each element</param>
        /// <param name="onError">The Action delegate to perform if an error is encountered</param>
        /// <exception cref="System.Exception">Generic exception; Catches any exception occurring in the Action Delegate</exception>
        /// <example>
        ///   <code>
        ///   <![CDATA[
        /// ObservableCollection<string> targetString = new ObservableCollection<string>() { "a", "b", "c", "d", "e", "f", "g" };
        /// Action<Exception> onError = (ex)=>
        /// {
        /// Handle the exception
        /// };
        /// Action<char> process = (arg) =>
        /// {
        /// };
        /// targetChar.ForEach(process, onError);
        /// ]]>
        ///   </code>
        ///   </example>
        /// <remarks>Performs a ForEach operation on each element of the collection. Each item is passed to the processing Action delegate.
        /// If an exception is raised during the execution and a Action delegate is provided then the exception is passed to it so that it can
        /// be handled. If no exception delegate is passed then the exception is thrown and will have to be handled. via your own exception handler.</remarks>
        public static void ForEach<T>(this ICollection<T> collection, Action<T> operation, Action<Exception> onError = null) where T : class
        {
            foreach (var item in collection)
            {
                try
                {
                    operation(item);
                }
                catch (Exception exception)
                {
                    if (onError != null)
                    {
                        onError(exception);
                    }
                    else
                    {
                        throw;
                    }
                }
            }
        }

        /// <summary>
        /// Performs the specified action on each element of the Array.
        /// </summary>
        /// <typeparam name="T">Generic Type Parameter</typeparam>
        /// <param name="array">string to perform the action on</param>
        /// <param name="operation">The Action delegate to perform on each element, null elements are not passed to the delegate</param>
        /// <param name="onError">The Action delegate to perform if an error is encountered</param>
        /// <exception cref="System.Exception">Generic exception; Catches any exception occurring in the Action Delegate</exception>
        /// <example>
        ///   <code>
        ///   <![CDATA[
        /// string[] targetString = new string[] { "a", "b", "c", "d", "e", "f", "g" };
        /// Action<Exception> onError = (ex)=>
        /// {
        /// Handle the exception
        /// };
        /// Action<char> process = (arg) =>
        /// {
        /// };
        /// targetChar.ForEach(process, onError);
        /// ]]>
        ///   </code>
        ///   </example>
        /// <remarks>Performs a ForEach operation on each element of the array. Each item is passed to the processing Action delegate.
        /// If an exception is raised during the execution and a Action delegate is provided then the exception is passed to it so that it can
        /// be handled. If no exception delegate is passed then the exception is thrown and will have to be handled. via your own exception handler.</remarks>
        public static void ForEach<T>(this Array array, Action<T> operation, Action<Exception> onError = null) where T : IConvertible
        {
            for (int i = 0; i < array.Length; i++)
            {
                if (array.GetValue(i) != null)
                {
                    try
                    {
                        operation((T)Convert.ChangeType(array.GetValue(i), typeof(T)));
                    }
                    catch (Exception exception)
                    {
                        if (onError != null)
                        {
                            onError(exception);
                        }
                        else
                        {
                            throw;
                        }
                    }
                }
            }
        }
    }
}
